package backend

import (
	"github.com/gorilla/websocket"
	"net/url"
	"sync"
	"time"
)

type Client struct {
	addr         string
	serverAddr   string
	rpc          *RpcMap
	mu           sync.Mutex
	ws           *websocket.Conn
	close        bool
	disconnected chan struct{}
	id           int
}

// todo: rename serverAddr to signalUrl
func Dial(addr string, serverAddr string) (*Client, error) {
	c := &Client{
		addr:         addr,
		serverAddr:   serverAddr,
		rpc:          NewRpcMap(),
		disconnected: make(chan struct{}, 0),
		id:           -1,
	}

	err := c.handleConnect()
	if err != nil {
		log.Warningf("could not dial |%s|, error: %s", addr, err)
		return nil, err
	}

	go c.handlePing()
	go c.handleMessage()

	return c, nil
}

func (c *Client) Close() {
	c.close = true
	if err := c.ws.WriteControl(websocket.CloseNormalClosure, []byte{}, time.Now().Add(kClientRWTimeout)); err != nil {
		log.Warningf("could not send close, force close")
		c.ws.Close()
		return
	}

	select {
	case <-time.After(kClientRWTimeout):
		log.Warningf("timeout, force close")
		c.ws.Close()
		return
	case <-c.disconnected:
	}
}

func (c *Client) RequestResponse(m *Message) (*Message, error) {
	call := c.rpc.NewCall()

	m.RpcId = call.id

	log.Debugf("send request [%d:%s]", m.RpcId, m.Op)

	c.mu.Lock()
	if err := c.ws.SetWriteDeadline(time.Now().Add(kClientRWTimeout)); err != nil {
		log.Warningf("could not set write deadline, error: %", err)
		c.mu.Unlock()
		call.Cancel()
		return nil, err
	}
	if err := c.ws.WriteJSON(m); err != nil {
		log.Warningf("could not write, error: %s", err)
		c.mu.Unlock()
		call.Cancel()
		return nil, err
	}
	c.mu.Unlock()

	rsp, err := call.WaitForResponse()
	if err != nil {
		log.Warningf("response timeout, error: %s", err)
		return nil, err
	}

	log.Debugf("receive [%d:%s]", rsp.RpcId, m.Op)

	return rsp, nil
}

func (c *Client) handlePing() {
	t := time.NewTicker(kClientPingPeriod)
	for {
		select {
		case now := <-t.C:
			log.Debugf("conn %d send ping", c.id)
			if err := c.ws.WriteControl(websocket.PingMessage, []byte{}, now.Add(kClientPingPeriod)); err != nil {
				log.Warningf("conn %d send ping, error: %s", c.id, err)
				return
			}
		case <-c.disconnected:
			if c.close {
				return
			}
		}
	}
}

func (c *Client) handleMessage() {
loop:
	for {
		if err := c.ws.SetReadDeadline(time.Now().Add(kClientRWTimeout)); err != nil {
			log.Warningf("conn %d could not set read deadline, error: %s", c.id, err)
			if c.close {
				close(c.disconnected)
				return
			}
		}
		msg := &Message{}
		if err := c.ws.ReadJSON(msg); err != nil {
			log.Warningf("conn %d could not read msg, error: %s", c.id, err)
			if c.close {
				close(c.disconnected)
				return
			}

			for {
				if err := c.handleConnect(); err == nil {
					continue loop
				}
				time.Sleep(kClientReConnectPeriod)
			}
		}

		switch msg.Op {
		case OpResponse:
			c.handleResponse(msg)
		default:
			log.Warningf("conn %d could not handle msg, %+v", c.id, msg)
		}
	}
}

func (c *Client) handleResponse(msg *Message) {
	call := c.rpc.FinishCall(msg.RpcId)
	call.NotifyResponse(msg)
}

func (c *Client) handleConnect() error {
	log.Info("connecting to proxy")

	u := url.URL{
		Scheme: "ws",
		Host:   c.addr,
		Path:   "/proxy",
	}

	ws, _, err := websocket.DefaultDialer.Dial(u.String(), nil)

	if err != nil {
		log.Warning("could not connect to proxy")
		return err
	}

	c.ws = ws

	ws.SetPongHandler(func(data string) error {
		log.Debug("receive pong")
		err := ws.SetReadDeadline(time.Now().Add(kClientRWTimeout))
		return err
	})

	ws.SetPingHandler(func(data string) error {
		log.Debug("recv ping, send pong")
		err := ws.WriteControl(websocket.PongMessage, []byte{}, time.Now().Add(kClientRWTimeout))
		return err
	})

	log.Info("connected to proxy")

	serverUrl := url.URL{
		Scheme: "http",
		Host:   c.serverAddr,
		Path:   "/rtc",
	}

	req := &Message{
		Op:        OpRegister,
		BackendId: c.id,
		Url:       serverUrl.String(),
	}

	c.mu.Lock()



	log.Infof("send register request, %+v", req)

	if err := c.ws.SetWriteDeadline(time.Now().Add(kClientRWTimeout)); err != nil {
		log.Warningf("could not set write deadline, error: %", err)
		c.mu.Unlock()
		return err
	}

	if err := c.ws.WriteJSON(req); err != nil {
		log.Warningf("could not write request, error: %s", err)
		c.mu.Unlock()
		return err
	}

	c.mu.Unlock()

	log.Info("wait for register response")

	if err := c.ws.SetReadDeadline(time.Now().Add(kClientRWTimeout)); err != nil {
		log.Warningf("could not set read deadline, error: %s", err)
		return err
	}

	rsp := &Message{}
	if err := c.ws.ReadJSON(rsp); err != nil {
		log.Warningf("could not read response, error: %s", err)
		return err
	}

	log.Infof("receive register response, %+v", rsp)

	if c.id != -1 && c.id != rsp.BackendId {
		log.Warningf("re-register id %d got different id %d, proxy was restarted", c.id, rsp.BackendId)
	}
	c.id = rsp.BackendId

	return err
}
